//使用UDP协议实现一个简单的服务器：
#pragma once  
#include<string>
#include<iostream>
#include<cstring>
#include<string.h>
#include<functional>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"log.hpp"

using namespace std;

typedef function<string(const string&)>func_t ;//创建一个名为 func_t 的新类型，它代表一个函数，这个函数接受一个常量字符串引用作为参数，并返回一个字符串。

Log lg;

enum //尽量多使用枚举类型代替宏定义
{
    SOCKET_ERR=1,
    BIND_ERR
};

uint16_t defaultport=8080;//默认的端口号，端口号要16位的整形
string default_ip="0.0.0.0";//默认点分十进制的服务器ip
const int size=1024;

//任何对象都是可以使用类进行描述的，包括服务器：
class UdpServer 
{
public: 
    UdpServer(const uint16_t &port=defaultport,const string &ip=default_ip):sockfd_(0),port_(port),ip_(ip),isrunning_(false)
    {}
    //服务器的初始化：
    void Init()
    {
        //第一步：创建udp的套接字socket(典型例子)
        sockfd_=socket(AF_INET,SOCK_DGRAM,0);//返回值就是打开的网卡文件描述符
        /*
        socket：这是创建套接字的函数。
        1,AF_INET：这是一个指定地址族的常量，表示 IPv4 地址。这是告诉系统我们想要创建一个基于 TCP/IP 协议族的套接字。
        2,SOCK_DGRAM：这是一个指定套接字类型的常量，表示我们想要创建一个数据报套接字，也就是 UDP 套接字。UDP 是一种无连接的协议，它允许发送和接收数据包，但不保证数据包的顺序或可靠性。
        3,0：这是指定协议的参数。在大多数情况下，这个参数被设置为 0，表示使用默认协议。对于 AF_INET 和 SOCK_DGRAM，这意味着使用 UDP 协议。
        */
       if(sockfd_<0)//检查socket函数的调用
       {
            lg(Fatal,"socket create error,sockfd:%d",sockfd_);
            exit(SOCKET_ERR);
       }
       lg(Info,"socket create success,sockfd:%d",sockfd_);

       //第二步：绑定bind：(也是惯用流程),也就是在本地系统上设置套接字的网络接口和端口，以便它可以监听和接收来自网络上特定地址和端口的传入连接或数据。
       struct sockaddr_in local;
       bzero(&local,sizeof(local));//先初始化为0
       local.sin_family=AF_INET;
       local.sin_port=htons(port_);//保证端口号是网络字节序列，因为要给对方发送
       local.sin_addr.s_addr=inet_addr(ip_.c_str());//inet_addr函数帮我们把点分十进制处理为网络字节序的二进制序列
       //local.sin_addr.s_addr=htonl(INADDR_ANY);//在sin_addr.s_addr字段设置INADDR_ANY时，应用程序可以接受发送到任何网络接口的数据。
       /*
        struct sockaddr_in local;：声明了一个 sockaddr_in 结构体变量 local，用于存储本地地址信息。
        1,bzero(&local, sizeof(local));：使用 bzero 函数将 local 结构体的所有成员初始化为零。bzero 是一个将给定的字节数设置为零的函数，这里用于确保结构体中的所有字段都被初始化。在现代 C++ 代码中，更推荐使用 memset 函数，因为 bzero 不是 C++ 标准的一部分。
        2,local.sin_family = AF_INET;：设置 sin_family 成员为 AF_INET，表示地址族是 IPv4。
        3,local.sin_port = htons(port_);：将 sin_port 成员设置为 htons 函数的返回值，htons 函数将主机字节序的端口号转换为网络字节序。这是必要的，因为网络通信中需要使用统一的字节序（大端序）。
        4,local.sin_addr.s_addr = inet_addr(ip_.c_str());：将 sin_addr 成员的 s_addr 字段设置为 inet_addr 函数的返回值，inet_addr 函数将点分十进制的 IP 地址字符串转换为网络字节序的二进制值。
       */
       //设置号该进程的本地信息之后就可以使用bind函数进行绑定了：
       //bind函数：bind 函数的作用是将一个套接字与一个本地地址（进程的ip和端口号）关联起来，这样传入的连接或数据报就会知道应该发送到哪个地址和端口。
       if(bind(sockfd_,(const struct sockaddr*)&local,sizeof(local))<0)
       {
            lg(Fatal,"bind error,errno:%d,err string:%s",errno,strerror(errno));
            exit(BIND_ERR);
       }
       lg(Info,"bind success");
    }

    //让服务器运行起来：
    void Run(func_t func)//让服务器运行起来需要传入一个函数指针（返回值为string，参数为string的引用）
    {
        isrunning_=true;
        char inbuffer[size];
        while(isrunning_)
        {
            struct sockaddr_in client;//客户端的地址端口等信息
            socklen_t len=sizeof(client);
            //使用recvfrom接收发送方的数据到指定的容器里
            ssize_t n=recvfrom(sockfd_,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&client,&len);//默认是阻塞的方式等待接收
            /*
            1,sockfd_：是之前创建的套接字文件描述符。
            2,inbuffer：是一个字符数组，用于存储接收到的数据。
            3,sizeof(inbuffer) - 1：指定了 inbuffer 缓冲区的大小减去1，以确保留出空间放置字符串结束符 \0，这样接收到的数据就可以被当作一个 C 字符串处理。
            4,0：是 flags 参数，通常设置为0，表示没有特殊的接收标志。
            5,输出型参数，(struct sockaddr*)&client：是一个指向 sockaddr 结构体的指针，用于存储发送方的地址信息。这里将 client 变量的地址转换为 sockaddr* 类型。
            6,&len：指向 len 变量的指针，用于存储实际的地址结构体的大小。
            */

           //接收信息失败时，不算特别严重的错误，就warning即可：
           if(n<0)
           {
                lg(Warning,"recvfrom error,errno:%d,err string:%s",errno,strerror(errno));
                continue;//继续下一轮的接收
           }
           inbuffer[n]=0;//把读到的数据的结尾补上\0，这样就可以当做字符串输出
           string info=inbuffer;
           string echo_string=func(info);//调用传进来的函数，得到func处理后的字符串

            //我们服务器完成一个小功能：把客户端发送的数据处理后发送回去
            //sendto 函数尝试将数据发送到由 client 指定的地址
           sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(const sockaddr*)&client,len);
           //我们把接收数据时的保存的客户端的信息结构体和该结构体的大小传到该函数中，这样该函数就可以找到并发送我们的目标字符串数据到对应的客户端
        }
    }
    ~UdpServer()
    {
        if(sockfd_>0)close(sockfd_);//析构的时候关闭套接字对应的文件描述符即可
    }

private: 
    int sockfd_;//网路文件描述符（我们理解为网卡文件）
    string ip_;//服务器ip
    uint16_t port_;//端口号，服务器进程的端口时候
    bool isrunning_;

};
//注意点：
//1,服务器运行起来后，可以使用功能netstat -naup查看udp协议的服务器
//2，ip地址表示唯一主机，端口号标识该主机唯一进程
//3,网络通信接口函数使用的都是struct sockaddr类型的结构体，我们使用的struct sockaddr_in*类型要强转为sockaddr*才能给接口函数使用
//4，云服务器禁止bind公网ip，虚拟机可以
//5,bind后凡是发送给这台主机的数据都要根据端口号向上交付
//6,0-1023号端口号是系统内定的端口号，我们一般使用1024+端口号
